Add a `search` command to cargo
authorJakub Bukaj <jakub@jakub.cc>
Sat, 22 Nov 2014 14:36:14 +0000 (09:36 -0500)
committerJakub Bukaj <jakub@jakub.cc>
Mon, 24 Nov 2014 13:59:58 +0000 (08:59 -0500)
src/bin/cargo.rs
src/bin/search.rs [new file with mode: 0644]
src/cargo/ops/mod.rs
src/cargo/ops/registry.rs
src/registry/lib.rs

index baeb90db301de80b68f6acb771e85d37477b5f2c..40d459b9b86dc8e0fe39b6b8d1c9f436a07d73b0 100644 (file)
@@ -74,6 +74,7 @@ macro_rules! each_subcommand( ($macro:ident) => ({
     $macro!(publish)
     $macro!(read_manifest)
     $macro!(run)
+    $macro!(search)
     $macro!(test)
     $macro!(update)
     $macro!(verify_project)
diff --git a/src/bin/search.rs b/src/bin/search.rs
new file mode 100644 (file)
index 0000000..95d01bc
--- /dev/null
@@ -0,0 +1,35 @@
+use cargo::ops;
+use cargo::core::{MultiShell};
+use cargo::util::{CliResult, CliError};
+
+#[deriving(Decodable)]
+struct Options {
+    flag_host: Option<String>,
+    flag_verbose: bool,
+    arg_query: String
+}
+
+pub const USAGE: &'static str = "
+Search packages in crates.io
+
+Usage:
+    cargo search [options] <query>
+
+Options:
+    -h, --help              Print this message
+    --host HOST             Host of a registry to search in
+    -v, --verbose           Use verbose output
+";
+
+pub fn execute(options: Options, shell: &mut MultiShell) -> CliResult<Option<()>> {
+    shell.set_verbose(options.flag_verbose);
+    let Options {
+        flag_host: host,
+        arg_query: query,
+        ..
+    } = options;
+
+    ops::search(query.as_slice(), shell, host)
+        .map(|_| None)
+        .map_err(|err| CliError::from_boxed(err, 101))
+}
index a23bd7dc209001e47df6793fcd378eb1f042deb6..53ade356e9f55c2843a526eb5ace5c27a1fa389c 100644 (file)
@@ -16,7 +16,7 @@ pub use self::lockfile::{write_lockfile, write_pkg_lockfile};
 pub use self::cargo_test::{run_tests, run_benches, TestOptions};
 pub use self::cargo_package::package;
 pub use self::registry::{publish, registry_configuration, RegistryConfig};
-pub use self::registry::{registry_login, http_proxy, http_handle};
+pub use self::registry::{registry_login, search, http_proxy, http_handle};
 pub use self::registry::{modify_owners, yank, OwnersOptions};
 pub use self::cargo_fetch::{fetch};
 pub use self::cargo_pkgid::pkgid;
index 1486771ea83a02ad8cb109d79930cc3b4053b9d9..c4fbeafaed93b3d2f796cd1261803efea64be946 100644 (file)
@@ -1,6 +1,7 @@
 use std::collections::HashMap;
 use std::io::File;
 use std::os;
+use term::color::BLACK;
 
 use curl::http;
 use git2;
@@ -12,7 +13,7 @@ use core::manifest::ManifestMetadata;
 use ops;
 use sources::{PathSource, RegistrySource};
 use util::config;
-use util::{CargoResult, human, internal, ChainError, Require, ToUrl};
+use util::{CargoResult, human, internal, ChainError, ToUrl};
 use util::config::{Config, ConfigValue, Location};
 
 pub struct RegistryConfig {
@@ -143,9 +144,7 @@ pub fn registry(shell: &mut MultiShell,
         token: token_config,
         index: index_config,
     } = try!(registry_configuration());
-    let token = try!(token.or(token_config).require(|| {
-        human("no upload token found, please run `cargo login`")
-    }));
+    let token = token.or(token_config);
     let index = index.or(index_config).unwrap_or(RegistrySource::default_url());
     let index = try!(index.as_slice().to_url().map_err(human));
     let sid = SourceId::for_registry(&index);
@@ -323,3 +322,46 @@ pub fn yank(manifest_path: &Path,
 
     Ok(())
 }
+
+pub fn search(query: &str, shell: &mut MultiShell, index: Option<String>) -> CargoResult<()> {
+    fn truncate_with_ellipsis(s: &str, max_length: uint) -> String {
+        if s.len() < max_length {
+            s.to_string()
+        } else {
+            format!("{}…", s[..max_length - 1])
+        }
+    }
+
+    let (mut registry, _) = try!(registry(shell, None, index));
+
+    let crates = try!(registry.search(query).map_err(|e| {
+        human(format!("failed to retrieve search results from the registry: {}", e))
+    }));
+
+    let list_items = crates.iter()
+        .map(|krate| (
+            format!("{} ({})", krate.name, krate.max_version),
+            krate.description.as_ref().map(|desc|
+                truncate_with_ellipsis(desc.replace("\n", " ").as_slice(), 128))
+        ))
+        .collect::<Vec<_>>();
+    let description_margin = list_items.iter()
+        .map(|&(ref left, _)| left.len() + 4)
+        .max()
+        .unwrap_or(0);
+
+    for (name, description) in list_items.into_iter() {
+        let line = match description {
+            Some(desc) => {
+                let space = String::from_char(
+                    description_margin - name.len(),
+                    ' ');
+                name + space + desc
+            }
+            None => name
+        };
+        try!(shell.say(line, BLACK));
+    }
+
+    Ok(())
+}
index 8768c592a439ca5c9dab7326eecc696ac47fcaf3..0e5be48fe94ea6bcd304a285195a159ee185a110 100644 (file)
@@ -13,21 +13,35 @@ use serialize::json;
 
 pub struct Registry {
     host: String,
-    token: String,
+    token: Option<String>,
     handle: http::Handle,
 }
 
 pub type Result<T> = result::Result<T, Error>;
 
+#[deriving(PartialEq)]
+pub enum Auth {
+    Authorized,
+    Unauthorized
+}
+
 pub enum Error {
     Curl(curl::ErrCode),
     NotOkResponse(http::Response),
     NonUtf8Body,
     Api(Vec<String>),
     Unauthorized,
+    TokenMissing,
     Io(io::IoError),
 }
 
+#[deriving(Decodable)]
+pub struct Crate {
+    pub name: String,
+    pub description: Option<String>,
+    pub max_version: String
+}
+
 #[deriving(Encodable)]
 pub struct NewCrate {
     pub name: String,
@@ -68,13 +82,14 @@ pub struct User {
 #[deriving(Decodable)] struct ApiError { detail: String }
 #[deriving(Encodable)] struct OwnersReq<'a> { users: &'a [&'a str] }
 #[deriving(Decodable)] struct Users { users: Vec<User> }
+#[deriving(Decodable)] struct Crates { crates: Vec<Crate> }
 
 impl Registry {
-    pub fn new(host: String, token: String) -> Registry {
+    pub fn new(host: String, token: Option<String>) -> Registry {
         Registry::new_handle(host, token, http::Handle::new())
     }
 
-    pub fn new_handle(host: String, token: String,
+    pub fn new_handle(host: String, token: Option<String>,
                       handle: http::Handle) -> Registry {
         Registry {
             host: host,
@@ -126,16 +141,23 @@ impl Registry {
                                                box tarball as Box<Reader>].into_iter());
 
         let url = format!("{}/api/v1/crates/new", self.host);
-        let response = handle(self.handle.put(url, &mut body)
-                                         .content_length(size)
-                                         .header("Authorization",
-                                                 self.token.as_slice())
-                                         .header("Accept", "application/json")
-                                         .exec());
+
+        let token = try!(self.token.as_ref().ok_or(Error::TokenMissing)).as_slice();
+        let request = self.handle.put(url, &mut body)
+            .content_length(size)
+            .header("Accept", "application/json")
+            .header("Authorization", token);
+        let response = handle(request.exec());
         let _body = try!(response);
         Ok(())
     }
 
+    pub fn search(&mut self, query: &str) -> Result<Vec<Crate>> {
+        let body = try!(self.req(format!("/crates?q={}", query), None, Get, Auth::Unauthorized));
+
+        Ok(json::decode::<Crates>(body.as_slice()).unwrap().crates)
+    }
+
     pub fn yank(&mut self, krate: &str, version: &str) -> Result<()> {
         let body = try!(self.delete(format!("/crates/{}/{}/yank", krate, version),
                                     None));
@@ -151,24 +173,28 @@ impl Registry {
     }
 
     fn put(&mut self, path: String, b: &[u8]) -> Result<String> {
-        self.req(path, Some(b), Put)
+        self.req(path, Some(b), Put, Auth::Authorized)
     }
 
     fn get(&mut self, path: String) -> Result<String> {
-        self.req(path, None, Get)
+        self.req(path, None, Get, Auth::Authorized)
     }
 
     fn delete(&mut self, path: String, b: Option<&[u8]>) -> Result<String> {
-        self.req(path, b, Delete)
+        self.req(path, b, Delete, Auth::Authorized)
     }
 
     fn req(&mut self, path: String, body: Option<&[u8]>,
-           method: Method) -> Result<String> {
+           method: Method, authorized: Auth) -> Result<String> {
         let mut req = Request::new(&mut self.handle, method)
                               .uri(format!("{}/api/v1{}", self.host, path))
-                              .header("Authorization", self.token.as_slice())
                               .header("Accept", "application/json")
                               .content_type("application/json");
+
+        let token = try!(self.token.as_ref().ok_or(Error::TokenMissing)).as_slice();
+        if authorized == Auth::Authorized {
+            req = req.header("Authorization", token);
+        }
         match body {
             Some(b) => req = req.body(b),
             None => {}
@@ -213,6 +239,7 @@ impl fmt::Show for Error {
                 write!(f, "api errors: {}", errs.connect(", "))
             }
             Error::Unauthorized => write!(f, "unauthorized API access"),
+            Error::TokenMissing => write!(f, "no upload token found, please run `cargo login`"),
             Error::Io(ref e) => write!(f, "io error: {}", e),
         }
     }